<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Controller
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Front.php 24593 2012-01-05 20:35:02Z matthew $
 */


/** Zend_Loader */
require_once 'Zend/Loader.php';

/** Zend_Controller_Action_HelperBroker */
require_once 'Zend/Controller/Action/HelperBroker.php';

/** Zend_Controller_Plugin_Broker */
require_once 'Zend/Controller/Plugin/Broker.php';

/**
 * @category   Zend
 * @package    Zend_Controller
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Controller_Front
{
  /**
   * Base URL
   * @var string
   */
  protected $_baseUrl = null;

  /**
   * Directory|ies where controllers are stored
   *
   * @var string|array
   */
  protected $_controllerDir = null;

  /**
   * Instance of Zend_Controller_Dispatcher_Interface
   * @var Zend_Controller_Dispatcher_Interface
   */
  protected $_dispatcher = null;

  /**
   * Singleton instance
   *
   * Marked only as protected to allow extension of the class. To extend,
   * simply override {@link getInstance()}.
   *
   * @var Zend_Controller_Front
   */
  protected static $_instance = null;

  /**
   * Array of invocation parameters to use when instantiating action
   * controllers
   * @var array
   */
  protected $_invokeParams = array();

  /**
   * Subdirectory within a module containing controllers; defaults to 'controllers'
   * @var string
   */
  protected $_moduleControllerDirectoryName = 'controllers';

  /**
   * Instance of Zend_Controller_Plugin_Broker
   * @var Zend_Controller_Plugin_Broker
   */
  protected $_plugins = null;

  /**
   * Instance of Zend_Controller_Request_Abstract
   * @var Zend_Controller_Request_Abstract
   */
  protected $_request = null;

  /**
   * Instance of Zend_Controller_Response_Abstract
   * @var Zend_Controller_Response_Abstract
   */
  protected $_response = null;

  /**
   * Whether or not to return the response prior to rendering output while in
   * {@link dispatch()}; default is to send headers and render output.
   * @var boolean
   */
  protected $_returnResponse = false;

  /**
   * Instance of Zend_Controller_Router_Interface
   * @var Zend_Controller_Router_Interface
   */
  protected $_router = null;

  /**
   * Whether or not exceptions encountered in {@link dispatch()} should be
   * thrown or trapped in the response object
   * @var boolean
   */
  protected $_throwExceptions = false;

  /**
   * Constructor
   *
   * Instantiate using {@link getInstance()}; front controller is a singleton
   * object.
   *
   * Instantiates the plugin broker.
   *
   * @return void
   */
  protected function __construct()
  {
    $this->_plugins = new Zend_Controller_Plugin_Broker();
  }

  /**
   * Enforce singleton; disallow cloning
   *
   * @return void
   */
  private function __clone()
  {
  }

  /**
   * Singleton instance
   *
   * @return Zend_Controller_Front
   */
  public static function getInstance()
  {
    if (null === self::$_instance) {
      self::$_instance = new self();
    }

    return self::$_instance;
  }

  /**
   * Resets all object properties of the singleton instance
   *
   * Primarily used for testing; could be used to chain front controllers.
   *
   * Also resets action helper broker, clearing all registered helpers.
   *
   * @return void
   */
  public function resetInstance()
  {
    $reflection = new ReflectionObject($this);
    foreach ($reflection->getProperties() as $property) {
      $name = $property->getName();
      switch ($name) {
        case '_instance':
          break;
        case '_controllerDir':
        case '_invokeParams':
          $this->{$name} = array();
          break;
        case '_plugins':
          $this->{$name} = new Zend_Controller_Plugin_Broker();
          break;
        case '_throwExceptions':
        case '_returnResponse':
          $this->{$name} = false;
          break;
        case '_moduleControllerDirectoryName':
          $this->{$name} = 'controllers';
          break;
        default:
          $this->{$name} = null;
          break;
      }
    }
    Zend_Controller_Action_HelperBroker::resetHelpers();
  }

  /**
   * Convenience feature, calls setControllerDirectory()->setRouter()->dispatch()
   *
   * In PHP 5.1.x, a call to a static method never populates $this -- so run()
   * may actually be called after setting up your front controller.
   *
   * @param string|array $controllerDirectory Path to Zend_Controller_Action
   * controller classes or array of such paths
   * @return void
   * @throws Zend_Controller_Exception if called from an object instance
   */
  public static function run($controllerDirectory)
  {
    self::getInstance()
      ->setControllerDirectory($controllerDirectory)
      ->dispatch();
  }

  /**
   * Add a controller directory to the controller directory stack
   *
   * If $args is presented and is a string, uses it for the array key mapping
   * to the directory specified.
   *
   * @param string $directory
   * @param string $module Optional argument; module with which to associate directory. If none provided, assumes 'default'
   * @return Zend_Controller_Front
   * @throws Zend_Controller_Exception if directory not found or readable
   */
  public function addControllerDirectory($directory, $module = null)
  {
    $this->getDispatcher()->addControllerDirectory($directory, $module);
    return $this;
  }

  /**
   * Set controller directory
   *
   * Stores controller directory(ies) in dispatcher. May be an array of
   * directories or a string containing a single directory.
   *
   * @param string|array $directory Path to Zend_Controller_Action controller
   * classes or array of such paths
   * @param  string $module Optional module name to use with string $directory
   * @return Zend_Controller_Front
   */
  public function setControllerDirectory($directory, $module = null)
  {
    $this->getDispatcher()->setControllerDirectory($directory, $module);
    return $this;
  }

  /**
   * Retrieve controller directory
   *
   * Retrieves:
   * - Array of all controller directories if no $name passed
   * - String path if $name passed and exists as a key in controller directory array
   * - null if $name passed but does not exist in controller directory keys
   *
   * @param  string $name Default null
   * @return array|string|null
   */
  public function getControllerDirectory($name = null)
  {
    return $this->getDispatcher()->getControllerDirectory($name);
  }

  /**
   * Remove a controller directory by module name
   *
   * @param  string $module
   * @return bool
   */
  public function removeControllerDirectory($module)
  {
    return $this->getDispatcher()->removeControllerDirectory($module);
  }

  /**
   * Specify a directory as containing modules
   *
   * Iterates through the directory, adding any subdirectories as modules;
   * the subdirectory within each module named after {@link $_moduleControllerDirectoryName}
   * will be used as the controller directory path.
   *
   * @param  string $path
   * @return Zend_Controller_Front
   */
  public function addModuleDirectory($path)
  {
    try {
      $dir = new DirectoryIterator($path);
    } catch (Exception $e) {
      require_once 'Zend/Controller/Exception.php';
      throw new Zend_Controller_Exception("Directory $path not readable", 0, $e);
    }
    foreach ($dir as $file) {
      if ($file->isDot() || !$file->isDir()) {
        continue;
      }

      $module = $file->getFilename();

      // Don't use SCCS directories as modules
      if (preg_match('/^[^a-z]/i', $module) || ('CVS' == $module)) {
        continue;
      }

      $moduleDir = $file->getPathname() . DIRECTORY_SEPARATOR . $this->getModuleControllerDirectoryName();
      $this->addControllerDirectory($moduleDir, $module);
    }

    return $this;
  }

  /**
   * Return the path to a module directory (but not the controllers directory within)
   *
   * @param  string $module
   * @return string|null
   */
  public function getModuleDirectory($module = null)
  {
    if (null === $module) {
      $request = $this->getRequest();
      if (null !== $request) {
        $module = $this->getRequest()->getModuleName();
      }
      if (empty($module)) {
        $module = $this->getDispatcher()->getDefaultModule();
      }
    }

    $controllerDir = $this->getControllerDirectory($module);

    if ((null === $controllerDir) || !is_string($controllerDir)) {
      return null;
    }

    return dirname($controllerDir);
  }

  /**
   * Set the directory name within a module containing controllers
   *
   * @param  string $name
   * @return Zend_Controller_Front
   */
  public function setModuleControllerDirectoryName($name = 'controllers')
  {
    $this->_moduleControllerDirectoryName = (string)$name;

    return $this;
  }

  /**
   * Return the directory name within a module containing controllers
   *
   * @return string
   */
  public function getModuleControllerDirectoryName()
  {
    return $this->_moduleControllerDirectoryName;
  }

  /**
   * Set the default controller (unformatted string)
   *
   * @param string $controller
   * @return Zend_Controller_Front
   */
  public function setDefaultControllerName($controller)
  {
    $dispatcher = $this->getDispatcher();
    $dispatcher->setDefaultControllerName($controller);
    return $this;
  }

  /**
   * Retrieve the default controller (unformatted string)
   *
   * @return string
   */
  public function getDefaultControllerName()
  {
    return $this->getDispatcher()->getDefaultControllerName();
  }

  /**
   * Set the default action (unformatted string)
   *
   * @param string $action
   * @return Zend_Controller_Front
   */
  public function setDefaultAction($action)
  {
    $dispatcher = $this->getDispatcher();
    $dispatcher->setDefaultAction($action);
    return $this;
  }

  /**
   * Retrieve the default action (unformatted string)
   *
   * @return string
   */
  public function getDefaultAction()
  {
    return $this->getDispatcher()->getDefaultAction();
  }

  /**
   * Set the default module name
   *
   * @param string $module
   * @return Zend_Controller_Front
   */
  public function setDefaultModule($module)
  {
    $dispatcher = $this->getDispatcher();
    $dispatcher->setDefaultModule($module);
    return $this;
  }

  /**
   * Retrieve the default module
   *
   * @return string
   */
  public function getDefaultModule()
  {
    return $this->getDispatcher()->getDefaultModule();
  }

  /**
   * Set request class/object
   *
   * Set the request object.  The request holds the request environment.
   *
   * If a class name is provided, it will instantiate it
   *
   * @param string|Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Exception if invalid request class
   * @return Zend_Controller_Front
   */
  public function setRequest($request)
  {
    if (is_string($request)) {
      if (!class_exists($request)) {
        require_once 'Zend/Loader.php';
        Zend_Loader::loadClass($request);
      }
      $request = new $request();
    }
    if (!$request instanceof Zend_Controller_Request_Abstract) {
      require_once 'Zend/Controller/Exception.php';
      throw new Zend_Controller_Exception('Invalid request class');
    }

    $this->_request = $request;

    return $this;
  }

  /**
   * Return the request object.
   *
   * @return null|Zend_Controller_Request_Abstract
   */
  public function getRequest()
  {
    return $this->_request;
  }

  /**
   * Set router class/object
   *
   * Set the router object.  The router is responsible for mapping
   * the request to a controller and action.
   *
   * If a class name is provided, instantiates router with any parameters
   * registered via {@link setParam()} or {@link setParams()}.
   *
   * @param string|Zend_Controller_Router_Interface $router
   * @throws Zend_Controller_Exception if invalid router class
   * @return Zend_Controller_Front
   */
  public function setRouter($router)
  {
    if (is_string($router)) {
      if (!class_exists($router)) {
        require_once 'Zend/Loader.php';
        Zend_Loader::loadClass($router);
      }
      $router = new $router();
    }

    if (!$router instanceof Zend_Controller_Router_Interface) {
      require_once 'Zend/Controller/Exception.php';
      throw new Zend_Controller_Exception('Invalid router class');
    }

    $router->setFrontController($this);
    $this->_router = $router;

    return $this;
  }

  /**
   * Return the router object.
   *
   * Instantiates a Zend_Controller_Router_Rewrite object if no router currently set.
   *
   * @return Zend_Controller_Router_Interface
   */
  public function getRouter()
  {
    if (null == $this->_router) {
      require_once 'Zend/Controller/Router/Rewrite.php';
      $this->setRouter(new Zend_Controller_Router_Rewrite());
    }

    return $this->_router;
  }

  /**
   * Set the base URL used for requests
   *
   * Use to set the base URL segment of the REQUEST_URI to use when
   * determining PATH_INFO, etc. Examples:
   * - /core
   * - /myapp
   * - /subdir/index.php
   *
   * Note that the URL should not include the full URI. Do not use:
   * - http://example.com/core
   * - http://example.com/myapp
   * - http://example.com/subdir/index.php
   *
   * If a null value is passed, this can be used as well for autodiscovery (default).
   *
   * @param string $base
   * @return Zend_Controller_Front
   * @throws Zend_Controller_Exception for non-string $base
   */
  public function setBaseUrl($base = null)
  {
    if (!is_string($base) && (null !== $base)) {
      require_once 'Zend/Controller/Exception.php';
      throw new Zend_Controller_Exception('Rewrite base must be a string');
    }

    $this->_baseUrl = $base;

    if ((null !== ($request = $this->getRequest())) && (method_exists($request, 'setBaseUrl'))) {
      $request->setBaseUrl($base);
    }

    return $this;
  }

  /**
   * Retrieve the currently set base URL
   *
   * @return string
   */
  public function getBaseUrl()
  {
    $request = $this->getRequest();
    if ((null !== $request) && method_exists($request, 'getBaseUrl')) {
      return $request->getBaseUrl();
    }

    return $this->_baseUrl;
  }

  /**
   * Set the dispatcher object.  The dispatcher is responsible for
   * taking a Zend_Controller_Dispatcher_Token object, instantiating the controller, and
   * call the action method of the controller.
   *
   * @param Zend_Controller_Dispatcher_Interface $dispatcher
   * @return Zend_Controller_Front
   */
  public function setDispatcher(Zend_Controller_Dispatcher_Interface $dispatcher)
  {
    $this->_dispatcher = $dispatcher;
    return $this;
  }

  /**
   * Return the dispatcher object.
   *
   * @return Zend_Controller_Dispatcher_Interface
   */
  public function getDispatcher()
  {
    /**
     * Instantiate the default dispatcher if one was not set.
     */
    if (!$this->_dispatcher instanceof Zend_Controller_Dispatcher_Interface) {
      require_once 'Zend/Controller/Dispatcher/Standard.php';
      $this->_dispatcher = new Zend_Controller_Dispatcher_Standard();
    }
    return $this->_dispatcher;
  }

  /**
   * Set response class/object
   *
   * Set the response object.  The response is a container for action
   * responses and headers. Usage is optional.
   *
   * If a class name is provided, instantiates a response object.
   *
   * @param string|Zend_Controller_Response_Abstract $response
   * @throws Zend_Controller_Exception if invalid response class
   * @return Zend_Controller_Front
   */
  public function setResponse($response)
  {
    if (is_string($response)) {
      if (!class_exists($response)) {
        require_once 'Zend/Loader.php';
        Zend_Loader::loadClass($response);
      }
      $response = new $response();
    }
    if (!$response instanceof Zend_Controller_Response_Abstract) {
      require_once 'Zend/Controller/Exception.php';
      throw new Zend_Controller_Exception('Invalid response class');
    }

    $this->_response = $response;

    return $this;
  }

  /**
   * Return the response object.
   *
   * @return null|Zend_Controller_Response_Abstract
   */
  public function getResponse()
  {
    return $this->_response;
  }

  /**
   * Add or modify a parameter to use when instantiating an action controller
   *
   * @param string $name
   * @param mixed $value
   * @return Zend_Controller_Front
   */
  public function setParam($name, $value)
  {
    $name = (string)$name;
    $this->_invokeParams[$name] = $value;
    return $this;
  }

  /**
   * Set parameters to pass to action controller constructors
   *
   * @param array $params
   * @return Zend_Controller_Front
   */
  public function setParams(array $params)
  {
    $this->_invokeParams = array_merge($this->_invokeParams, $params);
    return $this;
  }

  /**
   * Retrieve a single parameter from the controller parameter stack
   *
   * @param string $name
   * @return mixed
   */
  public function getParam($name)
  {
    if (isset($this->_invokeParams[$name])) {
      return $this->_invokeParams[$name];
    }

    return null;
  }

  /**
   * Retrieve action controller instantiation parameters
   *
   * @return array
   */
  public function getParams()
  {
    return $this->_invokeParams;
  }

  /**
   * Clear the controller parameter stack
   *
   * By default, clears all parameters. If a parameter name is given, clears
   * only that parameter; if an array of parameter names is provided, clears
   * each.
   *
   * @param null|string|array single key or array of keys for params to clear
   * @return Zend_Controller_Front
   */
  public function clearParams($name = null)
  {
    if (null === $name) {
      $this->_invokeParams = array();
    } elseif (is_string($name) && isset($this->_invokeParams[$name])) {
      unset($this->_invokeParams[$name]);
    } elseif (is_array($name)) {
      foreach ($name as $key) {
        if (is_string($key) && isset($this->_invokeParams[$key])) {
          unset($this->_invokeParams[$key]);
        }
      }
    }

    return $this;
  }

  /**
   * Register a plugin.
   *
   * @param  Zend_Controller_Plugin_Abstract $plugin
   * @param  int $stackIndex Optional; stack index for plugin
   * @return Zend_Controller_Front
   */
  public function registerPlugin(Zend_Controller_Plugin_Abstract $plugin, $stackIndex = null)
  {
    $this->_plugins->registerPlugin($plugin, $stackIndex);
    return $this;
  }

  /**
   * Unregister a plugin.
   *
   * @param  string|Zend_Controller_Plugin_Abstract $plugin Plugin class or object to unregister
   * @return Zend_Controller_Front
   */
  public function unregisterPlugin($plugin)
  {
    $this->_plugins->unregisterPlugin($plugin);
    return $this;
  }

  /**
   * Is a particular plugin registered?
   *
   * @param  string $class
   * @return bool
   */
  public function hasPlugin($class)
  {
    return $this->_plugins->hasPlugin($class);
  }

  /**
   * Retrieve a plugin or plugins by class
   *
   * @param  string $class
   * @return false|Zend_Controller_Plugin_Abstract|array
   */
  public function getPlugin($class)
  {
    return $this->_plugins->getPlugin($class);
  }

  /**
   * Retrieve all plugins
   *
   * @return array
   */
  public function getPlugins()
  {
    return $this->_plugins->getPlugins();
  }

  /**
   * Set the throwExceptions flag and retrieve current status
   *
   * Set whether exceptions encounted in the dispatch loop should be thrown
   * or caught and trapped in the response object.
   *
   * Default behaviour is to trap them in the response object; call this
   * method to have them thrown.
   *
   * Passing no value will return the current value of the flag; passing a
   * boolean true or false value will set the flag and return the current
   * object instance.
   *
   * @param boolean $flag Defaults to null (return flag state)
   * @return boolean|Zend_Controller_Front Used as a setter, returns object; as a getter, returns boolean
   */
  public function throwExceptions($flag = null)
  {
    if ($flag !== null) {
      $this->_throwExceptions = (bool)$flag;
      return $this;
    }

    return $this->_throwExceptions;
  }

  /**
   * Set whether {@link dispatch()} should return the response without first
   * rendering output. By default, output is rendered and dispatch() returns
   * nothing.
   *
   * @param boolean $flag
   * @return boolean|Zend_Controller_Front Used as a setter, returns object; as a getter, returns boolean
   */
  public function returnResponse($flag = null)
  {
    if (true === $flag) {
      $this->_returnResponse = true;
      return $this;
    } elseif (false === $flag) {
      $this->_returnResponse = false;
      return $this;
    }

    return $this->_returnResponse;
  }

  /**
   * Dispatch an HTTP request to a controller/action.
   *
   * @param Zend_Controller_Request_Abstract|null $request
   * @param Zend_Controller_Response_Abstract|null $response
   * @return void|Zend_Controller_Response_Abstract Returns response object if returnResponse() is true
   */
  public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null)
  {
    if (!$this->getParam('noErrorHandler') && !$this->_plugins->hasPlugin('Zend_Controller_Plugin_ErrorHandler')) {
      // Register with stack index of 100
      require_once 'Zend/Controller/Plugin/ErrorHandler.php';
      $this->_plugins->registerPlugin(new Zend_Controller_Plugin_ErrorHandler(), 100);
    }

    if (!$this->getParam('noViewRenderer') && !Zend_Controller_Action_HelperBroker::hasHelper('viewRenderer')) {
      require_once 'Zend/Controller/Action/Helper/ViewRenderer.php';
      Zend_Controller_Action_HelperBroker::getStack()->offsetSet(-80, new Zend_Controller_Action_Helper_ViewRenderer());
    }

    /**
     * Instantiate default request object (HTTP version) if none provided
     */
    if (null !== $request) {
      $this->setRequest($request);
    } elseif ((null === $request) && (null === ($request = $this->getRequest()))) {
      require_once 'Zend/Controller/Request/Http.php';
      $request = new Zend_Controller_Request_Http();
      $this->setRequest($request);
    }

    /**
     * Set base URL of request object, if available
     */
    if (is_callable(array($this->_request, 'setBaseUrl'))) {
      if (null !== $this->_baseUrl) {
        $this->_request->setBaseUrl($this->_baseUrl);
      }
    }

    /**
     * Instantiate default response object (HTTP version) if none provided
     */
    if (null !== $response) {
      $this->setResponse($response);
    } elseif ((null === $this->_response) && (null === ($this->_response = $this->getResponse()))) {
      require_once 'Zend/Controller/Response/Http.php';
      $response = new Zend_Controller_Response_Http();
      $this->setResponse($response);
    }

    /**
     * Register request and response objects with plugin broker
     */
    $this->_plugins
      ->setRequest($this->_request)
      ->setResponse($this->_response);

    /**
     * Initialize router
     */
    $router = $this->getRouter();
    $router->setParams($this->getParams());

    /**
     * Initialize dispatcher
     */
    $dispatcher = $this->getDispatcher();
    $dispatcher->setParams($this->getParams())
      ->setResponse($this->_response);

    // Begin dispatch
    try {
      /**
       * Route request to controller/action, if a router is provided
       */

      /**
       * Notify plugins of router startup
       */
      $this->_plugins->routeStartup($this->_request);

      try {
        $router->route($this->_request);
      } catch (Exception $e) {
        if ($this->throwExceptions()) {
          throw $e;
        }

        $this->_response->setException($e);
      }

      /**
       * Notify plugins of router completion
       */
      $this->_plugins->routeShutdown($this->_request);

      /**
       * Notify plugins of dispatch loop startup
       */
      $this->_plugins->dispatchLoopStartup($this->_request);

      /**
       *  Attempt to dispatch the controller/action. If the $this->_request
       *  indicates that it needs to be dispatched, move to the next
       *  action in the request.
       */
      do {
        $this->_request->setDispatched(true);

        /**
         * Notify plugins of dispatch startup
         */
        $this->_plugins->preDispatch($this->_request);

        /**
         * Skip requested action if preDispatch() has reset it
         */
        if (!$this->_request->isDispatched()) {
          continue;
        }

        /**
         * Dispatch request
         */
        try {
          $dispatcher->dispatch($this->_request, $this->_response);
        } catch (Exception $e) {
          if ($this->throwExceptions()) {
            throw $e;
          }
          $this->_response->setException($e);
        }

        /**
         * Notify plugins of dispatch completion
         */
        $this->_plugins->postDispatch($this->_request);
      } while (!$this->_request->isDispatched());
    } catch (Exception $e) {
      if ($this->throwExceptions()) {
        throw $e;
      }

      $this->_response->setException($e);
    }

    /**
     * Notify plugins of dispatch loop completion
     */
    try {
      $this->_plugins->dispatchLoopShutdown();
    } catch (Exception $e) {
      if ($this->throwExceptions()) {
        throw $e;
      }

      $this->_response->setException($e);
    }

    if ($this->returnResponse()) {
      return $this->_response;
    }

    $this->_response->sendResponse();
  }
}
